#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@file: box.py
@time: 2024-06-25 11:23
@desc:
### 箱柜管理
我要添加四个表格`boxs`表，`images`表、`box_tags`表、`box_types`表
#### 表关系描述
`boxs`表跟`images`表是一对多的关系， 级联删除，如果删除box，则删除与之关联的图片
`boxs`表跟`box_tags`表是多对多的关系
`box_types`表跟`boxs`表是一对多的关系，不级联删除，如果删除box_type，
则将因此失去关联的box关联到`永久不可删除的默认BoxType`上
#### 表字段描述
`images`表表需要维护的字段描述如下：
```text
imageUrl 字符串类型 全局唯一 一个图片url形如： https://s1.ax1x.com/2020/06/27/N6fc8O.png
```
`box_tags`表表需要维护的字段描述如下：
```text
tagName  字符串类型 全局唯一 形如：紧急交货
```
`box_types`表表需要维护的字段描述如下：
```text
typeName 字符串类型 全局唯一 形如：20GP内贸
创建表时，添加一条默认数据 typeName 为 默认BoxType， 这条数据永久不可删除
```

`boxs`表需要维护的字段描述如下：
```text
boxId   箱号 全局唯一 格式  TEMU4556673
boxType  箱柜类型，关联 box_type表
contact  属主 关联之前的 contacts表
area 存放场区 关联之前的 field_area表
remark 备注
```
### 需要你做的事
前提：contacts、field_stack、field_area已经创建完成，你可以查看之前的上下文。
- 请设计表结构，输出sql，并根据sql生成对应的ORM model模型。
- 并设计实现四个表的restful api
    - `boxs`表 需要实现`增删改查`、文件下载接口、文件上传接口
        - 查询获取`boxs`列表接口，具备根据`name` `phone` `address` `area.Id` `area.field_stack.id` `boxId` `boxTypeId`
          `contactId`、`remark`、`boxTagIds`查询的功能
            - 其中的`boxId`是字段boxId，不是表的主键Id
            - 其中的`name`可能是联系人名称`contact.name`、堆区名称`area.name`、场栈名称`area.field_stack.name`
            - 其中的`phone`可能是联系人电话`contact.phone`、场栈电话`area.field_stack.phone`
            - 其中的`address`可能是联系人的地址`contact.address`、堆区关联场栈的地址`area.field_stack.address`
            - `remark` 查询使用`包含`逻辑，也就是库表对应字段包含remark查询参数
            - `boxTagIds`查询参数是一个id数组，查询关联的boxs
        - 文件下载功能接口
            - 将`boxs`表中的数据，全部查询导出excel表格，sheet页名称为`Boxs`
                - excel包含的表头字段包括`boxId、boxTypeName、contactName、contactPhone、areaName、
                  areaFieldStackName、areaFieldStackAddress、boxImages、boxTags`，不需要索引id。
                    - boxId 是箱号
                    - boxTypeName 是箱柜类型名
                    - contactName 属主名
                    - contactPhone 属主电话
                    - areaName 堆区名
                    - areaFieldStackName 堆区所关联场栈的名称
                    - areaFieldStackAddress 堆区所关联场栈的地址
                    - boxImages 是相关关联的多个imageUrl 使用`空格`拼接
                    - boxTags 是相关关联的多个tagName 使用`|`拼接
                - 将`images`表、`box_tags`表、`box_types`表中的数据，全部查询导出excel表格，sheet页名称分别为`Images`、`BoxTags`、`BoxTypes`
                    - 这三个表内容比较简单，根据常规逻辑实现即可
        - 文件上传功能，刚好跟导出相反，是将excel表格中的数据，读出，插入到数据表中
            - 读取excel文件中sheet页`Images`、`BoxTags`、`BoxTypes`，向对应库表导入数据（不存在则插入，存在则不插入）：
            - 读取excel文件中sheet页`Boxs`，插入数据：
                - 插入数据前，第1步，先根据`areaFieldStackName areaFieldStackAddress`两个字段检查数据表`field_stack`，
                  如`field_stack`不存在，则新增field_stack，新建时 remark标注`导入时创建`
                - 第2步，根据`areaName`和`FieldStackId`（第一步查询或新建field_stack的索引Id）两个字段检查`field_area`，
                   如`field_area`不存在，则新增field_area，新建时 remark标注`导入时创建`
                - 第3步，根据`contactName`和`contactPhone`检查`contacts`表，不存在则创建，新建时 remark标注`导入时创建`
                - 第4步，将`boxImages、boxTags`，根据导出逻辑逆操作，拆分，检查对应表格，不存在则创建
                - 第5步，根据`boxTypeName`检查box_type表，不存在则创建
                - 最后根据BoxId检查box表中是否存在，如不存在，则创建插入，备注`导入时创建`，如存在，则更新已存在数据，备注`导入时更新`
    - `images`表、`box_tags`表、`box_types` 需要实现`增删改查`
"""
import traceback

from flask import Blueprint, request, jsonify, send_file, url_for
from io import BytesIO
import pandas as pd
from sqlalchemy import desc
from global_scope import MAX_PAGE_SIZE, get_db
from project_model import Box, Image, BoxType, BoxTag, Contact, FieldArea, FieldStack
from tools import log_operations, token_required, get_boxs_df, excel_to_boxs
from common_tools import to_dict, apply_formatting
from tools import get_contacts_df, get_field_stack_df, get_area_df
from tools import excel_to_contacts, excel_to_field_stacks, excel_to_areas
from tools import get_box_tags_df, excel_to_box_tags
from global_scope import contacts_sheet_name, field_stacks_sheet_name, areas_sheet_name
from global_scope import box_tags_sheet_name, box_types_sheet_name
from tools import get_box_types_df, excel_to_box_types
from global_scope import boxs_sheet_name
from tools import get_images_df, excel_to_images
from global_scope import images_sheet_name

db = get_db()

box_bp = Blueprint('box', __name__, url_prefix='/boxs')


@box_bp.route('', methods=['POST'])
@log_operations
@token_required
def add_box():
    data = request.json
    boxId = data.get('boxId')
    boxType_id = data.get('boxType_id')
    contact_id = data.get('contact_id')
    area_id = data.get('area_id')
    remark = data.get('remark')
    images = data.get('images', [])
    tags = data.get('tags', [])

    if not boxId or not boxType_id or not contact_id or not area_id:
        return jsonify({"code": 1, "message": "boxId, boxType_id, contact_id and area_id are required"}), 400

    box = Box.query.filter_by(boxId=boxId).first()
    if not box:
        box = Box(
            box_id=boxId, box_type_id=boxType_id, contact_id=contact_id, area_id=area_id, remark=remark)

    for image_id in images:
        image = Image.query.get(image_id)
        if image:
            box.images.append(image)

    for tag_id in tags:
        tag = BoxTag.query.get(tag_id)
        if tag:
            box.tags.append(tag)

    db.session.add(box)
    db.session.commit()

    return jsonify({"code": 0, "message": "Box added successfully", "data": box.id}), 201


@box_bp.route('/<int:box_id>', methods=['PATCH'])
@log_operations
@token_required
def update_box(box_id):
    data = request.json
    box = Box.query.get(box_id)
    if not box:
        return jsonify({"code": 1, "message": "Box not found"}), 404

    if 'boxId' in data:
        box.boxId = data['boxId']
    if 'boxType_id' in data:
        box.boxType_id = data['boxType_id']
    if 'contact_id' in data:
        box.contact_id = data['contact_id']
    if 'area_id' in data:
        field_area = FieldArea.query.get(data['area_id'])
        if (field_area.container_length == box.box_type.length or
                field_area.container_length == "不区分长度"):
            box.area_id = data['area_id']
        else:
            return jsonify({
                "code": 1,
                "message": f"箱柜类型长度为{box.box_type.length}与堆区长度{field_area.container_length}不匹配"},
            ), 400
    if 'remark' in data:
        box.remark = data['remark']

    if 'boxImages' in data:
        box.images = []
        for img in data['boxImages']:
            image = Image.query.get(img.get('id'))
            if image:
                box.images.append(image)

    if 'tags' in data:
        box.tags = []
        for tag_id in data['tags']:
            tag = BoxTag.query.get(tag_id)
            if tag:
                box.tags.append(tag)

    db.session.commit()

    return jsonify({"code": 0, "message": "Box updated successfully"}), 200


@box_bp.route('/<int:box_id>', methods=['DELETE'])
@log_operations
@token_required
def delete_box(box_id):
    box = Box.query.get(box_id)
    if not box:
        return jsonify({"code": 1, "message": "Box not found"}), 404

    db.session.delete(box)
    db.session.commit()

    return jsonify({"code": 0, "message": "Box deleted successfully"}), 200


@box_bp.route('/all', methods=['GET', 'POST'])
@log_operations
@token_required
def get_boxs():
    pageNo = request.json.get('pageNo', 1)
    pageSize = request.json.get('pageSize', MAX_PAGE_SIZE)
    filters = {
        'area_ids': request.json.get('areaIds'),
        'locations': request.json.get('locations'),
        'field_stack_ids': request.json.get('fieldStackIds'),
        'boxId': request.json.get('boxId'),
        'boxType_ids': request.json.get('boxTypeIds'),
        'contact_ids': request.json.get('contactIds'),
        'remark': request.json.get('remark'),
        'boxTagIds': request.json.get('boxTagIds'),
        'box_types': request.json.get('box_types'),
    }

    query = Box.query.join(Contact).join(FieldArea).join(FieldStack).join(BoxType)

    if filters['area_ids']:
        query = query.filter(Box.area_id.in_(filters['area_ids']))
    if filters['locations']:
        query = query.filter(FieldArea.location.in_(filters['locations']))
    if filters['field_stack_ids']:
        query = query.filter(FieldArea.field_stack_id.in_(filters['field_stack_ids']))
    if filters['boxId']:
        query = query.filter(Box.boxId.contains(filters['boxId']))
    if filters['boxType_ids']:
        query = query.filter(Box.boxType_id.in_(filters['boxType_ids']))
    if filters['box_types']:
        query = query.filter(BoxType.length.in_(filters['box_types']))
    if filters['contact_ids']:
        query = query.filter(Box.contact_id.in_(filters['contact_ids']))
    if filters['remark']:
        query = query.filter(Box.remark.contains(filters['remark']))
    if filters['boxTagIds']:
        query = query.filter(Box.tags.any(BoxTag.id.in_(filters['boxTagIds'])))

    # 添加分页逻辑
    pagination = query.order_by(desc(Box.id)).paginate(page=pageNo, per_page=pageSize, error_out=False)

    pageData = []
    for box in pagination.items:
        box_type_dict = dict()
        contact_dict = dict()
        area_dict = dict()
        if box.boxType_id:
            box_type_dict = to_dict(
                id=box.boxType_id, typeName=box.box_type.typeName,
                box_type=box.box_type.box_type, box_code=box.box_type.box_code,)
        if box.contact_id:
            contact_dict = to_dict(
                id=box.contact_id,
                name=box.contact.name,
                phone=box.contact.phone,
                value=f"{box.contact.name} | {box.contact.phone}"
            )
        if box.area_id:
            area_dict = to_dict(
                id=box.area_id,
                name=box.area.name,
                value=f"{box.area.name} | {box.area.field_stack.name} | {box.area.field_stack.address}",
                field_stack=to_dict(
                    id=box.area.field_stack.id,
                    name=box.area.field_stack.name,
                    address=box.area.field_stack.address,
                    value=f"{box.area.field_stack.name} | {box.area.field_stack.address}"
                )
            )
        box_data = {
            "id": box.id,
            "boxId": box.boxId,
            "boxType": box_type_dict,
            "contact": contact_dict,
            "area": area_dict,
            "boxImages": [to_dict(id=img.id, imageUrl=img.imageUrl) for img in box.images],
            "boxTags": [to_dict(id=tag.id, tagName=tag.tagName) for tag in box.tags],
            "remark": box.remark
        }
        pageData.append(box_data)

    # 构建响应体
    originUrl = url_for('box.get_boxs', **request.args.to_dict())  # 保留原始查询参数
    response = {
        "code": 0,
        "message": "OK",
        "data": {
            "pageData": pageData,
            "total":  pagination.total,
        },
        "originUrl": originUrl
    }

    return jsonify(response)


@box_bp.route('/download', methods=['GET'])
@log_operations
@token_required
def download_boxs():
    output = BytesIO()
    with pd.ExcelWriter(output, engine='openpyxl') as writer:
        df_objs = [
            (get_boxs_df(), boxs_sheet_name),
            (get_images_df(), images_sheet_name),
            (get_box_tags_df(), box_tags_sheet_name),
            (get_box_types_df(), box_types_sheet_name),
            (get_contacts_df(), contacts_sheet_name),
            (get_field_stack_df(), field_stacks_sheet_name),
            (get_area_df(), areas_sheet_name)
        ]
        # 写入数据
        for df, sheet_name in df_objs:
            df.to_excel(writer, index=False, sheet_name=sheet_name)
            apply_formatting(writer.sheets[sheet_name], df)

    output.seek(0)

    return send_file(output, download_name="boxs.xlsx", as_attachment=True)


@box_bp.route('/upload', methods=['POST'])
@log_operations
@token_required
def upload_boxs():
    if 'file' not in request.files:
        return jsonify({"code": 1, "message": "No file part"}), 400

    file = request.files['file']
    if file.filename == '':
        return jsonify({"code": 1, "message": "No selected file"}), 400

    try:
        excel_to_images(file, request)
        excel_to_box_tags(file, request)
        excel_to_box_types(file, request)
        excel_to_contacts(file, request)
        excel_to_field_stacks(file, request)
        excel_to_areas(file, request)

        new_records, updated_records = excel_to_boxs(file, request)
        return jsonify({
            "code": 0,
            "message": f"Boxs uploaded successfully: {new_records} added, {updated_records} updated"
        }), 201
    except Exception as e:
        tb = traceback.format_exc()
        request.trace_logger.error(f"Exception occurred: {str(e)}\n{tb}")
        return jsonify({"code": 1, "message": str(e)}), 500
